package com.owg.jigsaw.hierarchy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;

import com.owg.jigsaw.contour.BitmapContour;

import owg.util.data.property.Property;

public class TypeHierarchy
{	
	/**List of all available type nodes*/
	protected final ArrayList<TypeNode> values;
	/**Map of all available type nodes by their key objects*/
	protected final HashMap<Object, TypeNode> valueMap;
	/**List of all the hierarchy's roots. Uses lazy initialization.*/
	protected LinkedHashSet<TypeNode> roots = null;
	
	public final Property<Short> connectorWidth = new Property<Short>(Short.class, null);
	public final Property<Short> connectorHeight = new Property<Short>(Short.class, null);
	
	/**
	 * Create a new type hierarchy from the given type nodes. 
	 * @param vals All types that will be used in the hierarchy
	 */
	public TypeHierarchy(TypeNode... vals)
	{
		connectorWidth.set(BitmapContour.getDefaultSize(vals.length));
		connectorHeight.set(connectorWidth.get());
		
		this.values = new ArrayList<>(Arrays.asList(vals));
		this.valueMap = new HashMap<>();
		for (int i = 0; i < values.size(); i++)
		{
			TypeNode t = values.get(i);
			assert !t.isParentOf(t) : t+" cannot be its own parent";
			assert !t.isChildOf(t) : t+" cannot be its own child";

			assert (!t.hasContour()) : t+" has already been initialized! Cannot add it to "+this+"...";
			
			genContour(t, i);
			
			//Record the value in the hashmap so it can be found via its key later
			this.valueMap.put(t.getKey(), t);
		}
	}
	private void genContour(TypeNode t, int index)
	{
		short w = connectorWidth.get();
		short h = connectorHeight.get();
		byte[] raster = new byte[w*h];
		Arrays.fill(raster, (byte)1);
		
		int resolution = (int)Math.ceil(Math.sqrt(getNumValues()));
		//int cutWidth = 6;
		
		int upperEdgeCutThreshold = resolution;
		int lowerEdgeCutThreshold = resolution*2;
		
		for(int c = 0; c<getNumValues(); c++)
		{
			if(c == index || getValue(c).isParentOf(t))
			{
				int baseX = ((c%resolution)*w)/resolution;
				int endX = ((1+(c%resolution))*w)/resolution;
				if(c < upperEdgeCutThreshold)
				{
					for(int x = baseX; x<endX; x++)
					{
						int slope;
						if(c%2 == 0)
							slope = Math.min(h, endX-1-x);
						else
							slope = Math.min(h, x+1-baseX);
						
						for(int y = 0; y<slope; y++)
						{
							raster[x+y*w] = 0;
						}
					}
				}
				else if(c < lowerEdgeCutThreshold)
				{
					for(int x = baseX; x<endX; x++)
					{
						int slope;
						if(c%2 == 0)
							slope = Math.min(h, endX-1-x);
						else
							slope = Math.min(h, x+1-baseX);
						
						for(int y = 0; y<slope; y++)
						{
							raster[x+(h-1-y)*w] = 0;
						}
					}
				}
				else 
				{
					int avgCutSize = w/resolution+1;
					//Internal cuts
					int line = (c/resolution)-2;
					int y = avgCutSize+(line*(h-2*avgCutSize))/(resolution-2);
					for(int x = baseX; x<endX; x++)
					{
						raster[x+y*w] = 0;
					}
				}
			}
		}
		
		
		t.setContour(new BitmapContour(t, raster, w,h));
	}
	/**Create a TypeHierarchy using the given Java classes.*/
	public TypeHierarchy(Class<?>... classes)
	{
		this(toNodes(classes));
	}

	/**Create an array of TypeNodes for the given Java classes. */
	private static TypeNode[] toNodes(Class<?>[] classes)
	{
		TypeNode[] r = new TypeNode[classes.length];
		for(int i = 0; i<r.length; i++)
			r[i] = new JavaClassNode(classes[i]);
		return r;
	}
	/**Create a TypeNode for the given Java class. 
	 * <br/><br/>
	 * This part of the API is for use by the Cut Editor only.*/
	public static JavaClassNode toNode(Class<?> type)
	{
		return new JavaClassNode(type);
	}
	/**Return the type node instance with the given key. The type node must have been registered in the hierarchy when it was created.*/
	public TypeNode getTypeNode(Object key)
	{
		TypeNode v = valueMap.get(key);
		if(v == null)
			throw new IllegalStateException(this+" does not know of the type key \""+key+"\"");
		return v;
	}
	/**Return the number of types in the hierarchy*/
	public int getNumValues()
	{
		return values.size();
	}
	/**Return all parent types of the given type. The returned array can be modified without affecting the hierarchy itself.*/
	public TypeNode[] getParentsOf(TypeNode n)
	{
		ArrayList<TypeNode> r = new ArrayList<>();
		for(TypeNode p : values)
		{
			if(p.isParentOf(n))
				r.add(p);
		}
		return r.toArray(new TypeNode[r.size()]);
	}

	/**Return all the hierarchy's roots. The returned array can be modified without affecting the hierarchy itself.*/
	public TypeNode[] getRoots()
	{
		return roots.toArray(new TypeNode[roots.size()]);
	}
	/**Return the node with the given index.*/
	public TypeNode getValue(int index)
	{
		return values.get(index);
	}
	/**Return a new array containing all type nodes.*/
	public TypeNode[] getValues()
	{
		return values.toArray(new TypeNode[values.size()]);
	}
	private void findRoots()
	{
		if(roots == null)
		{
			roots = new LinkedHashSet<>();
			for (int i = 0; i < values.size(); i++)
			{
				TypeNode t = values.get(i);
				assert !t.isParentOf(t) : t+" cannot be its own parent";
				assert !t.isChildOf(t) : t+" cannot be its own child";
				
				boolean isTmpRoot = true;
				
				Iterator<TypeNode> iterator = roots.iterator();
				while (iterator.hasNext())
				{
					TypeNode r = iterator.next();
					if(t.isParentOf(r)) //r has a supertype so it is not a root, remove it
					{
						iterator.remove();
					}
					else if(t.isChildOf(r)) //t is a subtype so it cannot be root, don't add it
					{
						isTmpRoot = false;
					}
				}
				if(isTmpRoot)//Add t as a tentative root if no parents were found
					roots.add(t);
			}
		}
	}
	/**Whether the Type Node is a root in the hierarchy. Note that hierarchy can have multiple roots, but no cycles.*/
	public boolean isRoot(TypeNode t)
	{
		findRoots();
		return roots.contains(t);
	}
	
	public void invalidate()
	{
		roots = null;
	}
}
